package es.esi.gemde.modeltransformator.atlengine;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Vector;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.m2m.atl.core.ATLCoreException;
import org.eclipse.m2m.atl.core.IModel;
import org.eclipse.m2m.atl.core.IReferenceModel;
import org.eclipse.m2m.atl.core.emf.EMFExtractor;
import org.eclipse.m2m.atl.core.emf.EMFInjector;
import org.eclipse.m2m.atl.core.emf.EMFModelFactory;
import org.eclipse.m2m.atl.core.launch.ILauncher;
import org.eclipse.m2m.atl.engine.compiler.CompileTimeError;
import org.eclipse.m2m.atl.engine.compiler.atl2006.Atl2006Compiler;
import org.eclipse.m2m.atl.engine.emfvm.launch.EMFVMLauncher;
import org.eclipse.m2m.atl.engine.parser.AtlParser;

import es.esi.gemde.core.utils.GEMDEUtils;
import es.esi.gemde.modeltransformator.exceptions.TransformationEngineException;
import es.esi.gemde.modeltransformator.service.ITransformation;
import es.esi.gemde.modeltransformator.service.ITransformationEngine;

/**
 * Implementation of the GEMDE connector for ATL transformations
 * 
 * @author adrian.noguero@tecnalia.com
 *
 */
public class ATLEngine implements ITransformationEngine {
	
	public static final String ENGINE_NAME = "Atlas Transformation Language (ATL)";
	
	private EMFVMLauncher transformationLauncher;
	private EMFModelFactory modelFactory;
	private EMFInjector injector;
	private EMFExtractor extractor;

	/**
	 * Public constructor of the ATL Engine
	 */
	public ATLEngine() {
		transformationLauncher = new EMFVMLauncher();
		modelFactory = new EMFModelFactory();
		injector = new EMFInjector();
		extractor = new EMFExtractor();
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modeltransformator.service.ITransformationEngine#getName()
	 */
	@Override
	public String getName() {
		return ENGINE_NAME;
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modeltransformator.service.ITransformationEngine#checkTransformationURIValidity(java.util.List)
	 */
	@Override
	public boolean checkTransformationURIValidity(List<URI> uris) {
		if (uris == null || uris.size() == 0) {
			return false;
		}
		
		// Initialize
		Atl2006Compiler compiler = new Atl2006Compiler();
		File tempDir = GEMDEUtils.createDir(ATLEngineActivator.PLUGIN_ID);
		
		// Compile all ATL files
		int errorCount = 0;
		int modulesCount = 0;
		try {
			for (URI uri : uris) {
				File f = GEMDEUtils.uri2file(uri);
				FileInputStream fis = new FileInputStream(f);
				String out = tempDir.getAbsolutePath() + File.separator + f.getName().substring(0, f.getName().lastIndexOf('.')) + ".asm";
				CompileTimeError[] errors = compiler.compile(fis, out);
				errorCount += countErrors(errors);
				
				if (isModule(f)) {
					modulesCount++;
				}
				
			}
		}
		catch (IOException e) {
			GEMDEUtils.cleanupDir(ATLEngineActivator.PLUGIN_ID);
			return false;
		}
		
		// Cleanup & return
		GEMDEUtils.cleanupDir(ATLEngineActivator.PLUGIN_ID);
		
		if (errorCount > 0 || modulesCount != 1) {
			return false;
		}
		
		return true;
	}

	/* (non-Javadoc)
	 * @see es.esi.gemde.modeltransformator.service.ITransformationEngine#executeTransformation(org.eclipse.emf.ecore.EObject[], es.esi.gemde.modeltransformator.service.ITransformation, java.lang.String)
	 */
	@Override
	public IStatus executeTransformation(EObject[] inputs,
			ITransformation transformation, String outputPath)
			throws IllegalArgumentException, TransformationEngineException {
		
		// Initial checks
		if (inputs == null){
			throw new IllegalArgumentException("A null inputs array was provided");
		}
		
		if (transformation == null){
			throw new IllegalArgumentException("A null transformation was provided");
		}
		
		if (outputPath == null){
			throw new IllegalArgumentException("A null outputPath was provided");
		}
		
		if (transformation.getRequiredInputList().size() != inputs.length) {
			throw new IllegalArgumentException("The number of provided inputs doesn't match the number of required inputs for this transformation");
		}
		
		for (int i = 0; i < inputs.length; i++) {
			EClass c = transformation.getRequiredInputList().get(i);
			if (!(c.isInstance(inputs[i]))) {
				throw new IllegalArgumentException("Provided input \"" + inputs[i].eClass().getInstanceTypeName() + "\" doesn't match the required type \"" + c.getName() + "\"");
			}
		}
		
		// Get the options
		HashMap<String, EClass> inputMap = transformation.getInputs();
		HashMap<String, EClass> outputMap = transformation.getOutputs();
		
		if (inputMap == null || outputMap == null) {
			throw new IllegalArgumentException("Failed to get the required options for this kind of transformation. Please rebuild it.");
		}
		
		// Compile the ATL inputs to ASM
		Atl2006Compiler compiler = new Atl2006Compiler();
		AtlParser parser = AtlParser.getDefault();
		File tempDir = GEMDEUtils.createDir(ATLEngineActivator.PLUGIN_ID);
		
		HashMap<URI, String> atl2asmMap = new HashMap<URI, String>();
		File moduleFile = null;
		URI moduleURI = null;
		Vector<URI> libFiles = new Vector<URI>();
		
		// Compile all ATL files
		int errorCount = 0;
		int modulesCount = 0;
		String errorString = new String();
		try {
			for (URI uri : transformation.getTransformationURIs()) {
				File f = GEMDEUtils.uri2file(uri);
				FileInputStream fis = new FileInputStream(f);
				String out = tempDir.getAbsolutePath() + File.separator + f.getName().substring(0, f.getName().lastIndexOf('.')) + ".asm";
				CompileTimeError[] errors = compiler.compile(fis, out);
				errorCount += countErrors(errors);
				for (CompileTimeError err : errors) {
					errorString += err.getSeverity() + ": "+ err.getDescription() + "\n";
				}
				if (isModule(f)) {
					modulesCount++;
					moduleFile = f;
					moduleURI = uri;
				}
				else {
					libFiles.add(uri);
				}
				
				atl2asmMap.put(uri, out);
			}
		}
		catch (IOException e) {
			GEMDEUtils.cleanupDir(ATLEngineActivator.PLUGIN_ID);
			throw new TransformationEngineException(e);
		}
		
		if (errorCount != 0) {
			return new Status(IStatus.ERROR, ATLEngineActivator.PLUGIN_ID, "Provided transformation has errors: \n" + errorString);
		}
		
		if (modulesCount != 1) {
			return new Status(IStatus.ERROR, ATLEngineActivator.PLUGIN_ID, "Provided transformation has more than a module");
		}
		
		// Launch the transformation
		try {
			
			transformationLauncher.initialize(Collections.<String, Object> emptyMap());
			
			// Load the ASM module
			IModel module = parser.parseToModel(new FileInputStream(moduleFile));
			IReferenceModel atl = parser.getAtlMetamodel();
			
			Object mod = module.getElementsByType(atl.getMetaElementByName("Module")).size() > 0 ? module.getElementsByType(atl.getMetaElementByName("Module")).toArray()[0] : null;
			if (mod == null) {
				return new Status(IStatus.ERROR, ATLEngineActivator.PLUGIN_ID, "Failed to load the parsed Module"); 
			}
			
			// Load the ASM libraries
			// TODO
			for (URI libURI : libFiles) {
//				transformationLauncher.addLibrary(name, library)
			}
			
			// Load the metamodels of inputs and outputs
			List<EPackage> metamodels = getIOMetamodels(inputMap, outputMap);
			HashMap<EPackage, IReferenceModel> emf2atlMetamodels = new HashMap<EPackage, IReferenceModel>();
			for (EPackage mm : metamodels) {
				IReferenceModel atlMM = modelFactory.newReferenceModel();
				try {
					injector.inject(atlMM, mm.getNsURI());
					emf2atlMetamodels.put(mm, atlMM);
				}
				catch (ATLCoreException e) {
					throw new TransformationEngineException(e);
				}
			}
			
			// Load Refining Trace metamodel
			IReferenceModel refTraceMM = modelFactory.getBuiltInResource("RefiningTrace.ecore");
			IModel refiningTraceModel = modelFactory.newModel(refTraceMM);
			transformationLauncher.addOutModel(refiningTraceModel, "refiningTrace", "RefiningTrace");
			
			// Load Input models
			if (isRefiningTransformation(module)) {
				int index = 0;
				HashMap<String, IModel> ioModels = new HashMap<String, IModel>();
				for (Entry<String, EClass> e : inputMap.entrySet()) {
					String iname = e.getKey();
					EClass itype = e.getValue();
					EObject iobj = inputs[index];
					String ext = getModelExtension();
					String outputFile = outputPath + File.separator + iname + '.' + ext;

					IModel imodel = modelFactory.newModel(emf2atlMetamodels.get(itype.getEPackage()));
					String ipath = iobj.eResource().getURI().toString();
					if (ipath == null) {
						return new Status(IStatus.ERROR, ATLEngineActivator.PLUGIN_ID, "ATL does not support unsaved models. Failed to load the input model for input: " + iobj.toString());
					}
					//String fileName = new File(ipath).getName();
					injector.inject(imodel, ipath);
					transformationLauncher.addInOutModel(imodel, iname, getIOTypeName(iname, module));

					ioModels.put(outputFile, imodel);
					index++;
				}
				
				HashMap<String, Object> options = new HashMap<String, Object>();
				options.put("supportUML2Stereotypes", true);
				options.put("allowInterModelReferences", true);
				
				// Execute transformation
				transformationLauncher.launch(ILauncher.RUN_MODE, new NullProgressMonitor(), options, new FileInputStream(new File(atl2asmMap.get(moduleURI))));
				
				// Extract the output models to the output path
				for (Entry<String, IModel> e : ioModels.entrySet()) {
					extractor.extract(e.getValue(), org.eclipse.emf.common.util.URI.createFileURI(e.getKey()).toString());
				}
			}
			else {
				int index = 0;
				for (Entry<String, EClass> e : inputMap.entrySet()) {
					String iname = e.getKey();
					EClass itype = e.getValue();
					EObject iobj = inputs[index];

					IModel imodel = modelFactory.newModel(emf2atlMetamodels.get(itype.getEPackage()));
					String ipath = iobj.eResource().getURI().toString();
					if (ipath == null) {
						return new Status(IStatus.ERROR, ATLEngineActivator.PLUGIN_ID, "ATL does not support unsaved models. Failed to load the input model for input: " + iobj.toString());
					}
					//String fileName = new File(ipath).getName();
					injector.inject(imodel, ipath);
					transformationLauncher.addInModel(imodel, iname, getIOTypeName(iname, module));

					index++;
				}

				// Load Output models
				HashMap<String, IModel> outputModels = new HashMap<String, IModel>();
				for (Entry<String, EClass> e : outputMap.entrySet()) {
					String oname = e.getKey();
					EClass otype = e.getValue();

					IModel omodel = modelFactory.newModel(emf2atlMetamodels.get(otype.getEPackage()));
					transformationLauncher.addOutModel(omodel, oname, getIOTypeName(oname, module));

					outputModels.put(oname, omodel);
				}
				
				HashMap<String, Object> options = new HashMap<String, Object>();
				options.put("supportUML2Stereotypes", true);
				options.put("allowInterModelReferences", true);
				
				// Execute transformation
				transformationLauncher.launch(ILauncher.RUN_MODE, new NullProgressMonitor(), options, new FileInputStream(new File(atl2asmMap.get(moduleURI))));
				
				// Extract the output models to the output path
				for (Entry<String, IModel> e : outputModels.entrySet()) {
					String oname = e.getKey();
					IModel omodel = e.getValue();
					
					String ext = getModelExtension();
					
					String outputFile = outputPath + File.separator + oname + '.' + ext;
					
					extractor.extract(omodel, org.eclipse.emf.common.util.URI.createFileURI(outputFile).toString());
					
				}
			}
		}
		catch (ATLCoreException e) {
			throw new TransformationEngineException(e);
		}
		catch (IOException e) {
			throw new TransformationEngineException(e);
		}
		
		// Clean up and unload models and metamodels
		
		// Everything went OK!!
		return new Status(IStatus.OK,  ATLEngineActivator.PLUGIN_ID, "");
	}
	
	@Override
	public String[] getAdditionalOptionsList() {
		return new String[0];
	}

	@Override
	public boolean checkTransformationParamsValidity(
			HashMap<String, EClass> inputs, HashMap<String, EClass> outputs,
			HashMap<String, String> options) {
		return inputs.size() > 0 && outputs.size() > 0;
	}

	private String getModelExtension() {
		//FIXME Map<String, Object> exts = Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap();
		
		return "xmi";
	}

	private List<EPackage> getIOMetamodels(HashMap<String, EClass> inputMap,
			HashMap<String, EClass> outputMap) {
		Vector<EPackage> ret = new Vector<EPackage>();
		
		for (EClass c : inputMap.values()) {
			if (!ret.contains(c.getEPackage())) {
				ret.add(c.getEPackage());
			}
		}
		
		for (EClass c : outputMap.values()) {
			if (!ret.contains(c.getEPackage())) {
				ret.add(c.getEPackage());
			}
		}
		
		
		return ret;
	}

	private int countErrors(CompileTimeError[] errors) {
		int count = 0;
		for (CompileTimeError e : errors) {
			if (e.getSeverity().toUpperCase().equals("ERROR")) {
				count++;
			}
		}
		return count;
	}
	
	private boolean isRefiningTransformation(IModel module) {
		IReferenceModel atl = module.getReferenceModel();
		
		for (Object mod : module.getElementsByType(atl.getMetaElementByName("Module"))) {
			if (mod instanceof EObject && ((EObject)mod).eClass().getEStructuralFeature("isRefining") != null) {
				Boolean isRefining = (Boolean) ((EObject)mod).eGet(((EObject)mod).eClass().getEStructuralFeature("isRefining"));
				return isRefining;
			}
		}
		
		return false;
	}

	private boolean isModule(File f) throws IOException {
		AtlParser parser = AtlParser.getDefault();
		IModel model = null;
		try {
			model = parser.parseToModel(new FileInputStream(f));
		} catch (ATLCoreException e) {
			return false;
		}
		
		if (model == null) {
			return false;
		}
		
		IReferenceModel metamodel = model.getReferenceModel();
		return model.getElementsByType(metamodel.getMetaElementByName("Module")).size() == 1;
	}
	
	private String getIOTypeName(String ioName, IModel module) {
		String ret = "";
		IReferenceModel atl = module.getReferenceModel();
		
		for (Object io : module.getElementsByType(atl.getMetaElementByName("OclModel"))) {
			if (io instanceof EObject && ((EObject)io).eClass().getEStructuralFeature("name") != null) {
				String name = (String) ((EObject)io).eGet(((EObject)io).eClass().getEStructuralFeature("name"));
				if (name != null && name.equals(ioName)) {
					// Found the IO model
					EObject mm = (EObject) ((EObject)io).eGet(((EObject)io).eClass().getEStructuralFeature("metamodel"));
					ret = (String) mm.eGet(mm.eClass().getEStructuralFeature("name"));
				}
			}
		}
		
		return ret;
	}

}
